package com.leyou.search.service;

import com.fasterxml.jackson.core.type.TypeReference;
import com.leyou.common.enums.ExceptionEnum;
import com.leyou.common.exception.LyException;
import com.leyou.common.utils.BeanHelper;
import com.leyou.common.utils.JsonUtils;
import com.leyou.common.vo.PageResult;
import com.leyou.item.client.ItemClient;
import com.leyou.item.pojo.dto.*;
import com.leyou.search.dto.GoodsDTO;
import com.leyou.search.dto.SearchRequest;
import com.leyou.search.pojo.Goods;
import com.leyou.search.repository.GoodsRepository;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.Operator;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.LongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.StringTerms;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilterBuilder;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.stream.Collectors;

/**
 * 搜索数据处理
 */
@Service
public class SearchService {

    @Autowired
    private ItemClient itemClient;

    @Autowired
    private ElasticsearchTemplate esTemplate;  //Elasticsearch 模版对象

    @Autowired
    private GoodsRepository goodsRepository;

    /**
     * 将一个spu转换为一个Goods对象
     *
     * @param spu
     * @return
     */
    public Goods buildGoods(SpuDTO spu) {
        //商品相关搜索信息拼接：分类，品牌，名称，规格信息等

        //根据三级分类id查询分类名称，并将这些名称拼接成一个字符串
        String categoryNames = itemClient.queryCategoryByIds(spu.getCategoryIds()).
                stream().map(CategoryDTO::getName).collect(Collectors.joining(">"));

        //查询品牌名称
        String brandName = itemClient.queryBrandById(spu.getBrandId()).getName();

        //拼接 商品名称，分类名称，品牌名称
        String all = spu.getName() + categoryNames + brandName;

        //封装sku信息
        List<SkuDTO> skuList = itemClient.querySkuBySpuId(spu.getId());
        List<Map<String, Object>> skuMap = new ArrayList<>();
        for (SkuDTO sku : skuList) {
            Map<String, Object> map = new HashMap<>();
            map.put("id", sku.getId());
            map.put("title", sku.getTitle());
            map.put("price", sku.getPrice());
            map.put("image", StringUtils.substringBefore(sku.getImages(), ",")); //取第一张图片
            skuMap.add(map);
        }

        //将sku中的价格取出，放到set集合中，这价格不会重复
        Set<Long> priceSet = skuList.stream().map(SkuDTO::getPrice).collect(Collectors.toSet());

        //当前spu的规格参数
        Map<String, Object> specs = new HashMap<>();

        //查询可用于搜索的规格参数
        List<SpecParamDTO> specParams = itemClient.querySpecParams(null, spu.getCid3(), true);
        //查询商品详情信息
        SpuDetailDTO spuDetail = itemClient.querySpuDetailById(spu.getId());
        //通用规格参数值 ,将商品通用参数值放置到Map集合中，类似于 "1":"华为" ，key是"1"，value是"华为"
        Map<Long, Object> genericSpec = JsonUtils.toMap(spuDetail.getGenericSpec(), Long.class, Object.class);
        //特有规格参数值 与通用规格参数类似，只不过它的value可能是List集合
        Map<Long, List<String>> specialSpec = JsonUtils.nativeRead(spuDetail.getSpecialSpec(), new TypeReference<Map<Long, List<String>>>() {
        });

        //取出参数值
        //遍历 searching=1 的规格参数集合
        for (SpecParamDTO specParam : specParams) {
            //获取规格参数的名称
            String key = specParam.getName();
            //获取规格参数值
            Object value = null;
            //判断是否是通用规格参数  既能用于搜索，同时又是通用参数
            if (specParam.getGeneric()) {
                /*取出具体的可用于搜索的参数值*/
                value = genericSpec.get(specParam.getId());
            } else {
                /*特殊参数，但是可用于搜索，这时方法的返回值是一个字符串集合*/
                value = specialSpec.get(specParam.getId());
            }

            //判断是否是数字类型
            if (specParam.getNumeric()) {
                //是数字类型，分段
                value = chooseSegment(value, specParam);
            }
            //放入参数集合
            specs.put(key, value);
        }

        Goods goods = new Goods();
        goods.setAll(all);  // 商品名称，分类名称，品牌名称
        goods.setBrandId(spu.getBrandId());  //品牌id
        goods.setCategoryId(spu.getCid3());   //分类id
        goods.setCreateTime(spu.getCreateTime().getTime());  //创建时间，Long类型
        goods.setId(spu.getId());                   //商品id
        goods.setPrice(priceSet);                   //sku中的价格集合
        goods.setSkus(JsonUtils.toString(skuMap));  //sku信息集合，包含id,title,price,image
        goods.setSpecs(specs);                       //可用于搜索的参数集合
        goods.setSubTitle(spu.getSubTitle());        //副标题
        return goods;

    }

    /**
     * 为数字类型参数分段，并加上单位 例如将 2.0  变为 2.0-2.5英寸
     *
     * @param value
     * @param p
     * @return
     */
    private String chooseSegment(Object value, SpecParamDTO p) {
        //参数值为空
        if (value == null || StringUtils.isBlank(value.toString())) {
            return "其它";
        }

        double val = parseDouble(value);
        String result = "其它";

        //判断 value 所属的区间段
        for (String interval : p.getSegments().split(",")) {
            //将 1.0-2.0 变为 {1.0,2.0}
            String[] segs = interval.split("-");
            //构造开始值与结束值
            double begin = parseDouble(segs[0]);
            double end = Double.MAX_VALUE;
            //参数数组中的结束值不一定有，没有的话就直接设置为最大值
            if (segs.length == 2) {
                end = parseDouble(segs[1]);
            }

            //如果该值在区间内
            if (val >= begin && val < end) {
                //判断此区间的范围
                //1、参数区间在最后
                if (segs.length == 1) {
                    result = segs[0] + p.getUnit() + "以上";
                } else if (begin == 0) {   //2、参数区间不是最后，起始值为 0
                    result = segs[1] + p.getUnit() + "以下";
                } else {  //3、参数区间在中间
                    result = interval + p.getUnit();
                }
                break;
            }

        }

        return result;
    }

    /**
     * 将数值类型转换为double类型，并带有数字格式转换异常处理
     * @param value
     * @return
     */
    private double parseDouble(Object value) {
        try {
            return Double.parseDouble(value.toString());
        } catch (Exception e) {
            return 0;
        }
    }

    /**
     * 分页查询 从索引库中查询信息
     * 是 id subTitle  skus[id,image,title,price] 等信息
     * 这里的分页查询是查询条件与过滤条件查询的交集
     * @param request
     * @return
     */
    public PageResult<GoodsDTO> search(SearchRequest request) {

        String key = request.getKey();
        //如果搜索条件为 null
        if (StringUtils.isBlank(key)) {
            throw new LyException(ExceptionEnum.RESOURCE_NOT_FOUND);
        }

        //创建原生搜索构建器
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        //指定必须要包含的字段
        queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id", "subTitle", "skus"}, null));
        //todo : 匹配查询 字段all,key是搜索条件，  operator(Operator.AND)的作用是全匹配，不可将搜索条件进行分词，结果必须包含完整搜索条件
        //
        queryBuilder.withQuery(buildBasicQuery(request));

        //分页条件  elasticsearch中的分页从 0 开始
        Integer page = request.getPage() - 1;
        Integer size = request.getSize();
        //添加分页条件
        queryBuilder.withPageable(PageRequest.of(page, size));
        //执行搜索
        AggregatedPage<Goods> result = esTemplate.queryForPage(queryBuilder.build(), Goods.class);

        //数据条数
        long total = result.getTotalElements();
        //总页数
        int totalPages = result.getTotalPages();
        //所有数据
        List<Goods> goodsList = result.getContent();

        List<GoodsDTO> goodsDTOS = BeanHelper.copyWithCollection(goodsList, GoodsDTO.class);
        //封装结果并返回
        return new PageResult<>(total, totalPages, goodsDTOS);
    }

    /**
     *
     * 在查询与过滤之后，对所有参数都进行聚合，得到所有参数的值
     * 查询当前都有哪些规格参数
     *
     * @param request
     * @return
     */
    public Map<String, List<?>> queryFilters(SearchRequest request) {
        //创建过滤项集合   LinkedHashMap 可按照元素put顺序按序取出
        Map<String, List<?>> filterList = new LinkedHashMap<>();

        //构建查询条件
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        //封装过滤条件与查询条件
        QueryBuilder basicQuery = buildBasicQuery(request);
        //根据 key去all中全匹配查询
        queryBuilder.withQuery(basicQuery);
        //因为我们只是聚合，减少查询结果，所以查第一页，只查一条
        queryBuilder.withPageable(PageRequest.of(0, 1));
        //显示空的 source
        queryBuilder.withSourceFilter(new FetchSourceFilterBuilder().build());

        //根据分类和品牌进行聚合

        String categoryAgg = "categoryAgg";
        //分类id聚合  聚合桶名 categoryAgg ，聚合字段 分类id categoryId
        queryBuilder.addAggregation(AggregationBuilders.terms(categoryAgg).field("categoryId"));

        //品牌id聚合
        String brandAgg = "brandAgg";
        queryBuilder.addAggregation(AggregationBuilders.terms(brandAgg).field("brandId"));


        //查询
        AggregatedPage<Goods> result = esTemplate.queryForPage(queryBuilder.build(), Goods.class);

        //取出所有的聚合结果
        Aggregations aggregations = result.getAggregations();

        //取出分类聚合结果
        LongTerms cTerms = aggregations.get(categoryAgg);
        List<Long> categoryIds = handleCategoryAgg(cTerms, filterList);

        //取出品牌聚合结果
        LongTerms bTerms = aggregations.get(brandAgg);
        handleBrandAgg(bTerms, filterList);

        //如果只有一个分类
        if (categoryIds != null && categoryIds.size() == 1) {
            //查询规格参数集合 ，传入分类id，查询条件，结果集合
            handleSpecAgg(categoryIds.get(0), basicQuery, filterList);
        }

        return filterList;
    }

    /**
     * 规格参数聚合，聚合的结果是所有规格参数所有的值
     * 比如 参数操作系统，值有Android，其它，安卓等值
     * 规格参数聚合是在查询与过滤生效后再进行聚合的，
     * 所以才能达到用户点击某个参数值后，页面只会显示具有这个参数值属性的参数列表的效果了
     *
     * @param categoryId 分类id
     * @param basicQuery 查询条件
     * @param filterList 结果集合
     */
    public void handleSpecAgg(Long categoryId, QueryBuilder basicQuery, Map<String, List<?>> filterList) {
        //根据分类id查询规格参数
        List<SpecParamDTO> specParamDTOS = itemClient.querySpecParams(null, categoryId, true);


        //构建查询条件
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();

        //根据 key去all中全匹配查询
        queryBuilder.withQuery(basicQuery);
        //因为我们只是聚合，减少查询结果，所以查第一页，只查一条
        queryBuilder.withPageable(PageRequest.of(0, 1));
        //显示空的 source
        queryBuilder.withSourceFilter(new FetchSourceFilterBuilder().build());

        //聚合所有规格参数
        for (SpecParamDTO param : specParamDTOS) {
            String name = param.getName();
            //使用参数名当成聚合的桶名  每个参数名都为一个桶
            // specs.paramName.keyword：
            //      向索引库存入数据但未指定属性名时，索引库会自动生成属性名
            //      因为这些参数属性都属于specs
            queryBuilder.addAggregation(AggregationBuilders.terms(name).field("specs." + name + ".keyword"));
        }
        //查询
        AggregatedPage<Goods> goods = esTemplate.queryForPage(queryBuilder.build(), Goods.class);

        Aggregations aggregations = goods.getAggregations();


        //取出聚合结果
        for (SpecParamDTO param : specParamDTOS) {
            String name = param.getName();
            //todo : org.elasticsearch.search.aggregations.bucket.terms.UnmappedTerms cannot be cast to org.elasticsearch.search.aggregations.bucket.terms.StringTerms
            StringTerms specTerms = aggregations.get(name);

            List<String> specList = specTerms.getBuckets().stream().
                    map(StringTerms.Bucket::getKeyAsString).
                    filter(StringUtils::isNotEmpty).       //过滤掉空选项
                    collect(Collectors.toList());

            filterList.put(name, specList);
        }
    }

    /**
     * 从分类 聚合桶中查询出 分类id集合，再通过 分类id集合 查询分类集合
     * 返回分类id集合，如果大于一个分类，我们就不查询规格参数等信息，以免规格参数错乱
     *
     * @param cTerms
     * @param filterList
     */
    public List<Long> handleCategoryAgg(LongTerms cTerms, Map<String, List<?>> filterList) {
        List<Long> catrgoryIds = cTerms.getBuckets().stream().
                map(LongTerms.Bucket::getKeyAsNumber).   //取出id
                map(Number::longValue).
                collect(Collectors.toList());
        List<CategoryDTO> categoryDTOS = itemClient.queryCategoryByIds(catrgoryIds);
        filterList.put("分类", categoryDTOS);
        return catrgoryIds;
    }

    /**
     * 从品牌聚合桶中查询出 品牌id集合，再通过 品牌id集合 查询品牌集合
     *
     * @param bTerms
     * @param filterList
     */
    public void handleBrandAgg(LongTerms bTerms, Map<String, List<?>> filterList) {
        List<Long> brandIds = bTerms.getBuckets().stream().
                map(LongTerms.Bucket::getKeyAsNumber).
                map(Number::longValue).
                collect(Collectors.toList());
        List<BrandDTO> brandDTOS = itemClient.queryBrandByIds(brandIds);
        filterList.put("品牌", brandDTOS);
    }


    /**
     *
     * 添加查询条件与过滤条件
     *
     * @param request
     * @return
     */
    private QueryBuilder buildBasicQuery(SearchRequest request) {
        //构建bool查询
        BoolQueryBuilder queryBuilder = new BoolQueryBuilder();
        //必备查询
        queryBuilder.must(QueryBuilders.matchQuery("all", request.getKey()).operator(Operator.AND));

        //遍历键值对对象
        for (Map.Entry<String, String> entry : request.getFilter().entrySet()) {
            String key = entry.getKey();
            if ("品牌".equals(key)){
                key = "brandId";
            }else if ("分类".equals(key)){
                key = "categoryId";
            }else {
                key = "specs." + key + ".keyword";
            }

            queryBuilder.filter(QueryBuilders.termQuery(key,entry.getValue()));
        }


        return queryBuilder;
    }

    /**
     * 在索引库中添加上架商品
     * @param spuId
     */
    public void createIndex(Long spuId) {
        SpuDTO spuDTO = itemClient.querySpuById(spuId);

        Goods goods = buildGoods(spuDTO);

        goodsRepository.save(goods);
    }

    /**
     * 删除索引库中的商品
     * @param spuId
     */
    public void deleteIndex(Long spuId) {
        goodsRepository.deleteById(spuId);
    }
}
